#include <stdio.h>
#include <getopt.h>
#include <stdlib.h>
#include <string.h>

#define TAMANIO_BUFFER 512

enum SELECCION_DE_ALGORITMO {ALGORITMO_SELECCIONADO_MERGE_SORT=1,
							ALGORITMO_SELECCIONADO_SELECTION_SORT=2,
							ALGORITMO_NO_SELECCIONADO=3};

void swap(char* entrada, int pos1, int pos2) {
	char temp;
	
	temp = entrada[pos1];
	entrada[pos1] = entrada[pos2];
	entrada[pos2] = temp;
}

void selectionSort(char* entrada, int tamanioEntrada) {
	int i,j,min;

	for (i=0;i<tamanioEntrada;i++) {
		min=i;
		for (j=i+1;j<tamanioEntrada;j++) {
			if (entrada[j]<entrada[min])
				min=j;
		}
	if (i!=min)
		swap(entrada,i,min);
	}
}

void mergeBuffers ( char* buffer1, int tamanioBuffer1, 
					char* buffer2, int tamanioBuffer2, 
					char* salida) {
    int pBuffer1 = 0, pBuffer2 = 0, pSalida = 0;
	
    while (	pBuffer1 < tamanioBuffer1 && pBuffer2 < tamanioBuffer2) {
        if (buffer1[pBuffer1] < buffer2[pBuffer2]) {
            salida[pSalida] = buffer1[pBuffer1];
            pBuffer1++;
        } else {
            salida[pSalida] = buffer2[pBuffer2];
            pBuffer2++;
        }
        pSalida++;
    }

    while (pBuffer1 < tamanioBuffer1) {
        salida[pSalida] = buffer1[pBuffer1];
        pBuffer1++;
        pSalida++;
    }

    while (pBuffer2 < tamanioBuffer2) {
        salida[pSalida] = buffer2[pBuffer2];
        pBuffer2++;
        pSalida++;
    }    
}
 
void mergeSort(char* entrada, int tamanioEntrada) {
    char *buffer1 = NULL, *buffer2 = NULL;
    int tamanioBuffer1, tamanioBuffer2, pBuffer1, pBuffer2;
    
    if (tamanioEntrada > 1) {
		tamanioBuffer1 = (int) tamanioEntrada/2;
        if (tamanioEntrada%2 == 0) {            
			tamanioBuffer2 = tamanioBuffer1;
        } else {            
            tamanioBuffer2 = tamanioBuffer1 + 1;
        }

        buffer1 = (char *) malloc(sizeof(char) * tamanioBuffer1);
        buffer2 = (char *) malloc(sizeof(char) * tamanioBuffer2);
        
        for(pBuffer1 = 0; pBuffer1 < tamanioBuffer1; pBuffer1++)
            buffer1[pBuffer1] = entrada[pBuffer1];

        for(pBuffer2 = 0; pBuffer2 < tamanioBuffer2; pBuffer1++, pBuffer2++)
            buffer2[pBuffer2] = entrada[pBuffer1];
		// Divide los buffers creados recursivamente
        mergeSort(buffer1, tamanioBuffer1);
        mergeSort(buffer2, tamanioBuffer2);
		// Ordena los buffers
        mergeBuffers(buffer1, tamanioBuffer1, buffer2, tamanioBuffer2, entrada);

        free(buffer1);
        free(buffer2);
    }    
}

void mostrarAyuda () {
    printf("tp0 [OPTIONS][file...] \n");
    printf("%-20s %s\n", "-h, --help", "display this help and exit.");
    printf("%-20s %s\n", "-V, --version", "display version information and exit.");
    printf("%-20s %s\n", "-m, --merge", "use the mergesort algorithm.");
    printf("%-20s %s\n", "-s, --sel", "use the selectionsort algorithm.");
}

void mostrarVersion () {
	printf("Versión: 1.0\n");
}

FILE* abrirArchivo(const char *direccion,  const char * modo) {
	FILE *archivo = fopen(direccion, modo);
	if (!archivo) {
		fprintf(stderr,"No se pudo abrir el archivo: %s en modo %s\n",direccion, modo);
		exit(EXIT_FAILURE);
	}
	return archivo;
}

void ordenarConSelectionSort(char* entrada, char** salida, int tamanioDeDatos) {
	selectionSort(entrada, tamanioDeDatos);
	memcpy(*salida, entrada, tamanioDeDatos);
}

void ordenarConMergeSort(char* entrada, char** salida, int tamanioDeDatos) {
	mergeSort(entrada, tamanioDeDatos);
	memcpy(*salida, entrada, tamanioDeDatos);
}

void ordenarSegunAlgoritmoElegido (	enum SELECCION_DE_ALGORITMO seleccion,
									char* datosEntada,
									char** datosSalida,
									int tamanioDeDatos) {
	switch (seleccion) {
		case ALGORITMO_SELECCIONADO_SELECTION_SORT:
			ordenarConSelectionSort(datosEntada, datosSalida, tamanioDeDatos);
			break;
		default:
			// X defecto Merge-Sort.
			ordenarConMergeSort(datosEntada, datosSalida, tamanioDeDatos);
			break;
	}
}

void leerArchivoYObtenerDatos (	FILE* archivo,
								char **datosParseados,
								int *pTamanioTotalDeDatos) {

	char *buffer = (char*) malloc (sizeof(char) * TAMANIO_BUFFER);
	int bytesLeidos = 0 , bytesTotales = 0, numeroDeBuffers = 1, offset = 0;
	int tamanioAnteriorDeDatos = 0;

	while (!feof(archivo)) {
		bytesLeidos = fread(buffer + offset, sizeof(char), TAMANIO_BUFFER, archivo);
		bytesTotales += bytesLeidos;
		if (bytesLeidos == TAMANIO_BUFFER) {
			numeroDeBuffers ++;
			buffer = (char*)realloc (buffer,
									sizeof(char)* TAMANIO_BUFFER * numeroDeBuffers);
			offset += TAMANIO_BUFFER;
		}
	}

	tamanioAnteriorDeDatos = *(pTamanioTotalDeDatos);

	// Actualizo la nueva cantidad de datos
	(*pTamanioTotalDeDatos) += bytesTotales;
	(*datosParseados) = (char*) realloc (*datosParseados,
										sizeof(char)* (*pTamanioTotalDeDatos));

	memcpy ((*datosParseados) + tamanioAnteriorDeDatos, buffer, bytesTotales);
	free (buffer);
}

void escribirSalida (FILE* salida, char* datos, int tamanioTotalDeDatos) {
	fwrite(datos, sizeof(char), tamanioTotalDeDatos, salida);
}

int main (int argc, char **argv) {
	FILE *archivoDeEntradaStandar = stdin;
	int opcionElegida = 0, cantidadDeArchivosDeEntrada = 0, tamanioTotalDeDatos = 0;
	int *pTamanioTotalDeDatos = &tamanioTotalDeDatos;
	enum SELECCION_DE_ALGORITMO algoritmoSeleccionado = ALGORITMO_NO_SELECCIONADO;
	char *datosDeEntradas = NULL, *datosOrdenados = NULL;
	char **archivosDeEntrada = NULL;

	/* Una cadena que lista las opciones cortas válidas */
	const char* const opcionesCortas = "hvms";

	/* Una estructura de varios arrays describiendo las opciones largas */
	const struct option opcionesLargas[] = {
	  { "help",	  no_argument, NULL,'h'},
	  { "version", no_argument, NULL,'v'},
	  { "merge",no_argument, NULL,'m'},
	  { "sel",no_argument, NULL,'s'},
	  { NULL,     no_argument, NULL, 0 }
	};

	// Parseo de argumentos.
	while (1) {
		opcionElegida = getopt_long(argc, argv, opcionesCortas, opcionesLargas, NULL);

		if (opcionElegida == -1)
			break;

		switch (opcionElegida) {

			case 'h' : /* -h o --help */
				mostrarAyuda();
				exit(EXIT_SUCCESS);

			case 's' : /* -s o --sel */
				if (algoritmoSeleccionado == ALGORITMO_NO_SELECCIONADO)
					algoritmoSeleccionado = ALGORITMO_SELECCIONADO_SELECTION_SORT;
				break;

			case 'm' : /* -m  ó --merge */
				if (algoritmoSeleccionado == ALGORITMO_NO_SELECCIONADO)
					algoritmoSeleccionado = ALGORITMO_SELECCIONADO_MERGE_SORT;
			   break;

			case 'v': /* -v  ó --version */
				mostrarVersion();
				exit(EXIT_SUCCESS);

			default :
				mostrarAyuda();
				exit(EXIT_FAILURE);
		}
	}

	// Lectura de datos.
	archivosDeEntrada = argv + optind;
	cantidadDeArchivosDeEntrada = argc - optind;

	if (cantidadDeArchivosDeEntrada > 0) {
		archivoDeEntradaStandar = NULL;
		int i;
		for (i = 0; i<cantidadDeArchivosDeEntrada; ++i) {
			char *path = archivosDeEntrada[i];
			FILE* archivoDeEntrada = abrirArchivo(path, "rb");
			leerArchivoYObtenerDatos (	archivoDeEntrada,
										&datosDeEntradas,
										pTamanioTotalDeDatos);
			fclose(archivoDeEntrada);
		}
	}
	else {
		leerArchivoYObtenerDatos (	archivoDeEntradaStandar,
									&datosDeEntradas,
									pTamanioTotalDeDatos);
	}

	// Ordenar datos.
	datosOrdenados = (char*) malloc (sizeof(char) * tamanioTotalDeDatos);
	memset (datosOrdenados, 0, tamanioTotalDeDatos);

	ordenarSegunAlgoritmoElegido(	algoritmoSeleccionado,
									datosDeEntradas,
									&datosOrdenados,
									tamanioTotalDeDatos);

	// Escritura de datos.
	escribirSalida(stdout, datosOrdenados, tamanioTotalDeDatos);

	free (datosDeEntradas);
	free (datosOrdenados);
	return EXIT_SUCCESS;
}
